namespace CloudNinja.Metering
{
    using System;
    using System.Collections.Generic;
    using System.Reactive;
    using System.Reactive.Linq;

    using CloudNinja.Metering.BlobLeaseUtil;
    using CloudNinja.Metering.Common;
    using CloudNinja.Metering.Common.Logging;

    // Main design idea of this class is borrowed from 
    // Microsoft Patterns and Practices Azure Autoscaling block
    public sealed class TickTock : ITickTock
    {
        private readonly object lockObject;

        private readonly Func<TimeSpan, IObservable<Unit>> timerFactory;

        private readonly ILicenseToRun licenseToRun;

        private readonly Dictionary<string, WorkRegistration> tasks;

        private IDisposable executionLicenseSubscription;

        private readonly ILogger logger;

        public TickTock(ILicenseToRun licenseToRun, ILogger logger)
            : this(t => Observable.Interval(t).Select(_ => Unit.Default), licenseToRun, logger)
        {
        }

        private TickTock(Func<TimeSpan, IObservable<Unit>> workTimerFactory, ILicenseToRun licenseToRun, ILogger logger)
        {
            this.lockObject = new object();
            this.timerFactory = workTimerFactory;
            this.licenseToRun = licenseToRun;
            this.logger = logger;
            this.tasks = new Dictionary<string, WorkRegistration>();
        }

        public void Start()
        {
            lock (this.lockObject)
            {
                if (this.executionLicenseSubscription == null)
                {
                    this.executionLicenseSubscription = this.licenseToRun.StatusChanged.Subscribe(this.LicenseChanged);
                }

                this.licenseToRun.StartGetLicense();
                this.LicenseChanged(this.licenseToRun.Acquired);
            }
        }

        public void Stop()
        {
            lock (this.lockObject)
            {
                this.licenseToRun.StopGetLicense();
                if (this.executionLicenseSubscription != null)
                {
                    using (this.executionLicenseSubscription)
                    {
                        this.executionLicenseSubscription = null;
                    }
                }
            }
        }

        public void RegisterWork(string name, Action work, TimeSpan rerunInterval)
        {
            Helper.Guard(name, "name");
            Helper.Guard(name, "name");

            var registration = new WorkRegistration(work, rerunInterval, this.timerFactory, this.logger);

            lock (this.lockObject)
            {
                this.tasks.Add(name, registration);

                if (this.licenseToRun.Acquired)
                {
                    registration.Start();
                }
            }
        }

        public bool UnregisterWork(string name)
        {
            WorkRegistration work;

            lock (this.lockObject)
            {
                if (!this.tasks.TryGetValue(name, out work))
                {
                    return false;
                }

                this.tasks.Remove(name);
            }

            work.Stop();

            return true;
        }

        public void Dispose()
        {
            this.Stop();
        }

        private void LicenseChanged(bool status)
        {
            lock (this.lockObject)
            {
                if (status)
                {
                    foreach (var task in this.tasks.Values)
                    {
                        task.Start();
                    }
                }
                else
                {
                    foreach (var task in this.tasks.Values)
                    {
                        task.Stop();
                    }
                }
            }
        }
    }

    internal class WorkRegistration
    {
        private readonly Action work;
        private readonly TimeSpan rerunInterval;
        private readonly Func<TimeSpan, IObservable<Unit>> timerFactory;
        private readonly ILogger logger;

        private IDisposable timerSubscription;

        public WorkRegistration(Action work, TimeSpan rerunInterval, Func<TimeSpan, IObservable<Unit>> timerFactory, ILogger logger)
        {
            this.work = work;
            this.rerunInterval = rerunInterval;
            this.timerFactory = timerFactory;
            this.logger = logger;
        }

        public void Start()
        {
            lock (this.work)
            {
                if (this.timerSubscription == null)
                {
                    this.timerSubscription =
                        this.timerFactory.Invoke(this.rerunInterval).Subscribe(w =>
                            {
                                try
                                {
                                    this.work.Invoke();
                                }
                                catch (Exception exception)
                                {
                                    this.logger.LogException(exception);
                                }
                            });
                }
            }
        }

        public void Stop()
        {
            lock (this.work)
            {
                using (this.timerSubscription)
                {
                    this.timerSubscription = null;
                }
            }
        }
    }
}